package com.sky.aop;

import com.sky.anno.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * 自定义切面，实现公共字段自动填充处理逻辑
 * 1.AutoFill: 一个自定义注解，用来标记需要自动填充公共字段的方法。
 * 2.OperationType: 一个枚举类型，表示数据库操作类型，包括 INSERT 和 UPDATE。
 * 3.joinPoint: 切面通知方法的参数，可以获取到被拦截的方法的相关信息。
 * 4.MethodSignature: 代表方法的签名信息，可以获取到方法的参数类型、返回类型等信息。
 * 5.getMethod(): 获取被拦截的方法对象。
 * 6.getAnnotation(AutoFill.class): 获取被拦截的方法上的 AutoFill 注解对象。
 * 7.args: 获取被拦截的方法的参数列表。
 * 8.entity: 被拦截的方法的第一个参数，通常是一个实体对象。
 * 9.LocalDateTime.now(): 获取当前时间。
 * 10.BaseContext.getCurrentId(): 获取当前用户的 ID。
 * 11.getDeclaredMethod(): 获取指定名称和参数类型的方法对象，可以用于反射调用方法。
 * 12.invoke(): 反射调用指定对象的指定方法，并传入参数。
 */
@Aspect//一个切面类
@Component//Spring组件
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点
     * 这是一个切入点的声明，表示所有com.sky.mapper包中带有@AutoFill注解的方法都会被拦截，autoFillPointCut是这个切入点的名称。
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.anno.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 这是一个前置通知，表示在切入点方法执行前执行该方法。JoinPoint参数是切入点方法的连接点，可以获取到切入点方法的信息和参数。
     * @param joinPoint
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段自动填充...");

        //这是获取拦截的方法上的@AutoFill注解，以获取当前数据库操作类型
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
        OperationType operationType = autoFill.value();//获得数据库操作类型

        //这是获取拦截方法的参数，其中args是一个数组，里面存放了所有的方法参数。
        // 这里假设拦截的方法只有一个参数，且该参数是要被自动填充的实体对象。
        Object[] args = joinPoint.getArgs();
        if(args == null || args.length == 0){
            return;
        }

        Object entity = args[0];

        //准备赋值的数据,这是获取当前时间和当前用户ID，用于赋值到实体对象的公共字段上。
        LocalDateTime now = LocalDateTime.now();//获取当前时间，用于赋值到实体对象的公共字段上。
        Long currentId = BaseContext.getCurrentId();//：获取当前用户ID，用于赋值到实体对象的公共字段上。

        //根据当前不同的操作类型，为对应的属性通过反射来赋值  INSERT表示插入操作   UPDATE表示更新操作
        if(operationType == OperationType.INSERT){
            //为4个公共字段赋值
            try {
                //获取实体对象中的 setCreateTime 方法，用于设置实体对象的创建时间属性。
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                //获取实体对象中的 setCreateUser 方法，用于设置实体对象的创建用户 ID 属性。
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                //获取实体对象中的 setUpdateTime 方法，用于设置实体对象的更新时间属性。
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                //获取实体对象中的 setUpdateUser 方法，用于设置实体对象的更新用户 ID 属性。
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值
                setCreateTime.invoke(entity,now);//通过反射为实体对象的创建时间属性赋值。
                setCreateUser.invoke(entity,currentId);//通过反射为实体对象的创建用户 ID 属性赋值。
                setUpdateTime.invoke(entity,now);//通过反射为实体对象的更新时间属性赋值。
                setUpdateUser.invoke(entity,currentId);//通过反射为实体对象的更新
            } catch (Exception e) {//如果try块中抛出了任何异常，这个catch块将捕捉这个异常，并在控制台上输出异常信息。
                e.printStackTrace();
            }
        }else if(operationType == OperationType.UPDATE){//如果操作类型为“更新”，则执行下面的代码块。
            //为2个公共字段赋值
            try {
                /**
                 * 在这个else if块中，代码使用Java反射API来访问实体对象的setUpdateTime()和setUpdateUser()方法。
                 * 它们是用来更新实体对象的两个公共字段：updateTime和updateUser。
                 * 该代码使用了entity对象的getClass()方法获取实体对象的Class对象，
                 * 使用该对象的getDeclaredMethod()方法获取指定方法名和参数类型的Method对象，
                 * 然后使用Method对象的invoke()方法来调用实体对象的相应方法，并传入now和currentId参数。
                 * 如果调用过程中出现任何异常，将在控制台上输出异常信息。
                 */
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}